DateUtilsAdapter   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 31
eloc 159
dl 0
loc 231
rs 9.92
c 0
b 0
f 0

21 Functions

Rating   Name   Duplication   Size   Complexity  
A getFirstDayOfYear 0 3 1
A getWeekDaysOfMonth 0 10 1
A getYear 0 3 1
A getEasterDate 0 19 1
A isAWorkingDay 0 18 4
A format 0 3 1
A addDaysToDate 0 3 1
A getWorkedDaysDuringAPeriod 0 26 4
A getCurrentDate 0 3 1
A getDaysInMonth 0 3 1
A makeDate 0 34 1
A getMonth 0 3 1
A getLastDayOfYear 0 3 1
A getWorkedFreeDays 0 20 1
A getCurrentDateToISOString 0 3 1
A getLeaveDuration 0 21 3
A isWeekend 0 3 1
A getNumberOfPaidLeaveWeeks 0 3 1
A getLeaveDurationAsDays 0 7 1
A getWorkedDaysPerWeek 0 3 1
A getLeaveReferencePeriodDays 0 17 3
1
import { Injectable } from '@nestjs/common';
2
import {
3
  format as fnsFormat,
4
  isWeekend as fnsIsWeekend,
5
  getDaysInMonth as fnsGetDaysInMonth,
6
  eachDayOfInterval,
7
  addDays,
8
  isWeekend
9
} from 'date-fns';
10
import { MonthDate } from 'src/Application/Common/MonthDate';
11
import { IDateUtils } from 'src/Application/IDateUtils';
12
13
@Injectable()
14
export class DateUtilsAdapter implements IDateUtils {
15
  private makeDate(year: number, month: number, day: number): Date {
16
    /**
17
     * This creates a date in UTC timezone.
18
     *
19
     * The intent is to remedy possible timezone issues.
20
     *
21
     * Indeed, you might be tempted to write code like this:
22
     *
23
     * ```js
24
     * const d = new Date(year, month, day);
25
     * ```
26
     *
27
     * Sadly, this is wrong.
28
     *
29
     * Indeed, if your computer's (or the server's) timezone is not UTC, then
30
     * the actual `day` stored in `d` might be different.
31
     *
32
     * For example, if your computer is on UTC+1 (Paris time), then..
33
     *
34
     * ```js
35
     * > new Date(2022, 11, 14).toISOString() // Dec 14th, 2022
36
     * 2022-12-13T23:00:00.000Z // Oops, it was stored as Dec 13th!
37
     * ```
38
     *
39
     * (Yes, timezones are a bit of a pain.)
40
     *
41
     * To remedy this, we create a date from an ISO date string (yyyy-mm-dd),
42
     * with leading zero padding and all that.
43
     *
44
     * JavaScript's `Date()` will properly interpret this as an UTC-timezoned date.
45
     */
46
    const fMonth = String(month).padStart(2, '0');
47
    const fDay = String(day).padStart(2, '0');
48
    return new Date(`${year}-${fMonth}-${fDay}`);
49
  }
50
51
  public format(date: Date, format: string): string {
52
    return fnsFormat(date, format);
53
  }
54
55
  public getDaysInMonth(date: Date): number {
56
    return fnsGetDaysInMonth(date);
57
  }
58
59
  public getWeekDaysOfMonth(date: Date): Date[] {
60
    return eachDayOfInterval({
61
      start: new Date(date.getFullYear(), date.getUTCMonth(), 1),
62
      end: new Date(
63
        date.getFullYear(),
64
        date.getUTCMonth(),
65
        this.getDaysInMonth(date)
66
      )
67
    }).filter(day => !isWeekend(day));
68
  }
69
70
  public isWeekend(date: Date): boolean {
71
    return fnsIsWeekend(date);
72
  }
73
74
  public isAWorkingDay(date: Date): boolean {
75
    if (this.isWeekend(date)) {
76
      return false;
77
    }
78
79
    const workedFreeDays = this.getWorkedFreeDays(this.getYear(date));
80
    const formatedDate = this.format(date, 'yyyy-MM-dd');
81
82
    for (const day of workedFreeDays) {
83
      const formatedDay = this.format(day, 'yyyy-MM-dd');
84
85
      if (formatedDate === formatedDay) {
86
        return false;
87
      }
88
    }
89
90
    return true;
91
  }
92
93
  public getCurrentDate(): Date {
94
    return new Date();
95
  }
96
97
  public getYear(date: Date): number {
98
    return date.getUTCFullYear();
99
  }
100
101
  public getMonth(date: Date): MonthDate {
102
    return new MonthDate(date.getUTCFullYear(), date.getUTCMonth() + 1);
103
  }
104
105
  public getLastDayOfYear(date: Date): Date {
106
    return this.makeDate(this.getYear(date), 12, 31);
107
  }
108
109
  public getFirstDayOfYear(date: Date): Date {
110
    return this.makeDate(this.getYear(date), 1, 1);
111
  }
112
113
  public getCurrentDateToISOString(): string {
114
    return this.getCurrentDate().toISOString();
115
  }
116
117
  public addDaysToDate(date: Date, days: number): Date {
118
    return addDays(date, days);
119
  }
120
121
  public getWorkedDaysDuringAPeriod(start: Date, end: Date): Date[] {
122
    const dates: Date[] = [];
123
    const workedFreeDays: Date[] = [];
124
125
    for (let year = this.getYear(start); year <= this.getYear(end); year++) {
126
      workedFreeDays.push(...this.getWorkedFreeDays(year));
127
    }
128
129
    for (let day of eachDayOfInterval({ start, end })) {
130
      // date-fns returns local-timezone dates. Be sure to convert to
131
      // UTC-timezoned dates for ISO string comparison with work-free days.
132
      day = new Date(this.format(day, 'yyyy-MM-dd'));
133
134
      if (
135
        this.isWeekend(day) ||
136
        workedFreeDays.filter(d => d.toISOString() === day.toISOString())
137
          .length > 0
138
      ) {
139
        continue;
140
      }
141
142
      dates.push(day);
143
    }
144
145
    return dates;
146
  }
147
148
  public getWorkedFreeDays(year: number): Date[] {
149
    const fixedDays: Date[] = [
150
      this.makeDate(year, 1, 1), // New Year's Day
151
      this.makeDate(year, 5, 1), // Labour Day
152
      this.makeDate(year, 5, 8), // Victory in 1945
153
      this.makeDate(year, 7, 14), // National Day
154
      this.makeDate(year, 8, 15), // Assumption
155
      this.makeDate(year, 11, 1), // All Saints' Day
156
      this.makeDate(year, 11, 11), // The Armistice
157
      this.makeDate(year, 12, 25) // Christmas
158
    ];
159
160
    const easterDate = this.getEasterDate(year);
161
    const easterDays: Date[] = [
162
      addDays(easterDate, 1), // Easter Monday
163
      addDays(easterDate, 39) // Ascension
164
    ];
165
166
    return [...fixedDays, ...easterDays];
167
  }
168
169
  public getEasterDate(year: number): Date {
170
    const a = year % 19;
171
    const b = Math.floor(year / 100);
172
    const c = year % 100;
173
    const d = Math.floor(b / 4);
174
    const e = b % 4;
175
    const f = Math.floor((b + 8) / 25);
176
    const g = Math.floor((b - f + 1) / 3);
177
    const h = (19 * a + b - d - g + 15) % 30;
178
    const i = Math.floor(c / 4);
179
    const k = c % 4;
180
    const l = (32 + 2 * e + 2 * i - h - k) % 7;
181
    const m = Math.floor((a + 11 * h + 22 * l) / 451);
182
    const n0 = h + l + 7 * m + 114;
183
    const n = Math.floor(n0 / 31);
184
    const p = (n0 % 31) + 1;
185
186
    return this.makeDate(year, n, p);
187
  }
188
189
  public getLeaveDuration(
190
    startDate: string,
191
    isStartsAllDay: boolean,
192
    endDate: string,
193
    isEndsAllDay: boolean
194
  ): number {
195
    let duration = this.getWorkedDaysDuringAPeriod(
196
      new Date(startDate),
197
      new Date(endDate)
198
    ).length;
199
200
    if (false === isStartsAllDay) {
201
      duration -= 0.5;
202
    }
203
204
    if (false === isEndsAllDay && duration > 0.5) {
205
      duration -= 0.5;
206
    }
207
208
    return duration;
209
  }
210
211
  /**
212
   * @param duration Duration in minutes
213
   * @returns Duration in days
214
   */
215
  public getLeaveDurationAsDays(duration: number) {
216
    return duration / 420;
217
  }
218
219
  getLeaveReferencePeriodDays(date: Date): [Date, Date] {
220
    // Reference period is between June 1st and May 31st.
221
222
    let startDate = this.makeDate(this.getYear(date), 6, 1);
223
224
    if (startDate > date) {
225
      startDate = this.makeDate(this.getYear(date) - 1, 6, 1);
226
    }
227
228
    let endDate = this.makeDate(this.getYear(date), 5, 31);
229
230
    if (endDate < date) {
231
      endDate = this.makeDate(this.getYear(date) + 1, 5, 31);
232
    }
233
234
    return [startDate, endDate];
235
  }
236
237
  getWorkedDaysPerWeek(): number {
238
    return 5;
239
  }
240
241
  getNumberOfPaidLeaveWeeks(): number {
242
    return 7;
243
  }
244
}
245